using System;
using System.Net;
using System.IO;
using System.Text;
using System.Net.Sockets;

/*	-----------------------------------------------------------------------	
	Copyright:	umlungu consulting (pty) ltd
	Author:		Alan Benington	
	Started:	2006-07-18	
	Status:		release	
	Version:	2.0.0
	Build:		20070524
	License:	GNU General Public License
	-----------------------------------------------------------------------	*/
	
/*	-----------------------------------------------------------------------	
	Development Notes:
	==================
	20060718:
	Originally from FTPFactory.cs
	Written by Jaimon Mathew (jaimonmathew@rediffmail.com)
	20070524:
	Modification for .Net 2.0
	-----------------------------------------------------------------------	*/
	
namespace umlungu.engineX.FtpX {
	/// <summary>
	/// umlungu engineX blog ftp class.
	/// </summary>
	public class x_ftp
	{
		#region Invisible properties
		private string mes;
		private int bytes;
		private Socket clientSocket;
		private static int BLOCK_SIZE = 512;
		private int retValue;
		private Boolean debug;
		private Boolean logined;
		#endregion

		#region Visible properties
		private string reply, _message = "";
		/// <summary>Definiton of the property</summary>
		/// <value>Description of the value of the property</value>
		public string Message	{
			get	{	return _message;	}	
			set	{	_message = value;	}
		}
		
		private string remoteHost;
		/// <summary>name of the FTP server to connect to.</summary>
		/// <value>Description of the value of the property</value>
		public string RemoteHost	{
			get	{	return remoteHost;	}	
			set	{	remoteHost = value;	}
		}
		
		private int remotePort;
		/// <summary>port number to use for FTP.</summary>
		/// <value>Description of the value of the property</value>
		public int RemotePort	{
			get	{	return remotePort;	}	
			set	{	remotePort = value;	}
		}
		
		private string remotePath;
		/// <summary>The remote directory path.</summary>
		/// <value>Description of the value of the property</value>
		public string RemotePath	{
			get	{	return remotePath;	}	
			set	{	remotePath = value;	}
		}
		
		private string remoteUser;
		/// <summary>The user name to use for logging into the remote server.</summary>
		/// <value>Description of the value of the property</value>
		public string RemoteUser	{
			//get	{	return remoteUser;	}	
			set	{	remoteUser = value;	}
		}
		
		private string remotePass;
		/// <summary>The password to user for logging into the remote server.</summary>
		/// <value>Description of the value of the property</value>
		public string RemotePass	{
			//get	{	return remotePass;	}	
			set	{	remotePass = value;	}
		}
		Byte[] buffer = new Byte[BLOCK_SIZE];
		Encoding ASCII = Encoding.ASCII;
		#endregion

		#region Constructors/Destructors
		/// <overloads>Constructors</overloads>
		/// <summary>Default constructor</summary>
		public x_ftp() {
			remoteHost  = "localhost";
			remotePath  = ".";
			remoteUser  = "anonymous";
			remotePass  = "jaimon@school2000.co.uk";
			remotePort  = 21;
			debug     = false;
			logined    = false;
		}
		/// <summary>Constructor with complete connection parameters</summary>
		/// <param name="host">The host address</param>
		/// <param name="path">The path on the remote host</param>
		/// <param name="user">Username for login</param>
		/// <param name="pass">Password for login</param>
		/// <param name="port">Port on which the host responds to ftp</param>
		/// <param name="wantdebug">Debug flag</param>
		public x_ftp(string host, string path, string user, string pass, int port, bool wantdebug) {
			remoteHost  = host;
			remotePath  = path;
			remoteUser  = user;
			remotePass  = pass;
			remotePort  = port;
			debug     = wantdebug;
			logined    = false;
		}
		#endregion

		#region Public methods
		/// <summary>Return a string array containing the remote directory's file list.</summary>
		/// <param name="mask">File filter mask</param>
		public string[] FileList(string mask) {
			return(fileList("NLST", mask));
		}
		/// <summary>Return a string array containing the remote directory list.</summary>
		/// <param name="mask">File filter mask</param>
		public string[] DirList(string mask) {
			return(fileList("LIST", mask));
		}
		private string[] fileList(string cmd, string mask) {
			if(!logined)
				login();
			Socket cSocket = createDataSocket();
			sendCommand(cmd + " " + mask);
			
			if(!(retValue == 150 || retValue == 125))
				throw(new x_exception("xFtp::getFileList:" + retValue.ToString(), reply + _message));
			mes = "";
			while(true) {
				int bytes = cSocket.Receive(buffer, buffer.Length, 0);
				mes += ASCII.GetString(buffer, 0, bytes);
				if(bytes < buffer.Length)
					break;
			}
			//_Debug(mes);
			char[] seperator = {'\n'};
			string[] mess = mes.Split(seperator);
			cSocket.Close();
			readReply();
	
			if(retValue != 226)
				throw(new x_exception("xFtp::getFileList:" + retValue.ToString(), reply + _message));
			return mess;
		}

		/// <summary>Return the size of a file.</summary>
		/// <param name="fileName">The name of the remote file</param>
		public long getFileSize(string fileName) {
			if(!logined)
				login();
			sendCommand("SIZE " + fileName);
			long size=0;
			if(retValue == 213)
				size = Int64.Parse(reply.Substring(4));
			else
				throw(new x_exception("xFtp::getFileSize:" + retValue.ToString(), reply));
			return size;
		}

		/// <summary>Login to the remote server.</summary>
		public void login() {
			clientSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
			IPEndPoint ep = new IPEndPoint(Dns.GetHostEntry(remoteHost).AddressList[0], remotePort);
			try {
				clientSocket.Connect(ep);
			} catch(Exception) {
				throw(new x_exception("xFtp::login:", "Couldn't connect to remote server"));
			}
			readReply();
			if(retValue != 220) {
				close();
				throw(new x_exception("xFtp::login:" + retValue.ToString(), reply));
			}
			if(debug)
				_Debug("USER "+remoteUser);
				
			sendCommand("USER "+remoteUser);
			
			if( !(retValue == 331 || retValue == 230) ) {
				cleanup();
				throw(new x_exception("xFtp::login:" + retValue.ToString(), reply));
			}
	
			if( retValue != 230 ) {
				if(debug)
					_Debug("PASS xxx");
				sendCommand("PASS "+remotePass);
				if( !(retValue == 230 || retValue == 202) ) {
					cleanup();
					throw(new x_exception("xFtp::login:" + retValue.ToString(), reply));
				}
			}
			logined = true;
			_Debug("Connected to "+remoteHost);
			chdir(remotePath);
		}

		/// <summary>If the value of mode is true, set binary mode for downloads, else, set Ascii mode.</summary>
		/// <param name="mode">Mode flag</param>
		public void setBinaryMode(Boolean mode) {
			if(mode)
				sendCommand("TYPE I");
			else
				sendCommand("TYPE A");
			if (retValue != 200)
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
		}

		/// <summary>Download a file to the Assembly's local directory, keeping the same file name.</summary>
		/// <param name="remFileName">The name of the remote file</param>
		public void download(string remFileName) {
			download(remFileName,"",false);
		}
		/// <summary>set the resume flag.</summary>
		/// <param name="remFileName">The name of the remote file</param>
		/// <param name="resume">Resume flag</param>
		public void download(string remFileName,Boolean resume) {
			download(remFileName,"",resume);
		}
		/// <summary>Download a remote file to a local file name which can include a path. The local file name will be created or overwritten, but the path must exist.</summary>
		/// <param name="remFileName">The name of the remote file</param>
		/// <param name="locFileName">The name of the local file</param>
		public void download(string remFileName,string locFileName) {
			download(remFileName,locFileName,false);
		}
		/// <summary>Download a remote file to a local file name which can include a path, and set the resume flag. The local file name will be created or overwritten, but the path must exist.</summary>
		/// <param name="remFileName">The name of the remote file</param>
		/// <param name="locFileName">The name of the local file</param>
		/// <param name="resume">Resume flag</param>
		public void download(string remFileName,string locFileName,Boolean resume) {
			if(!logined)
				login();
	
			setBinaryMode(true);
			_Debug("Downloading file "+remFileName+" from "+remoteHost + "/"+remotePath);
	
			if (locFileName.Equals(""))
				locFileName = remFileName;
	
			if(!File.Exists(locFileName)) {
				Stream st = File.Create(locFileName);
				st.Close();
			}
			FileStream output = new FileStream(locFileName,FileMode.Open);
			Socket cSocket = createDataSocket();
			long offset = 0;
			if(resume) {
				offset = output.Length;
				if(offset > 0 ) {
					sendCommand("REST "+offset);
					if(retValue != 350) {
						//throw(new x_exception("xFtp::" + retValue.ToString(), reply));
						//Some servers may not support resuming.
						offset = 0;
					}
				}
				if(offset > 0) {
					if(debug)
						_Debug("seeking to " + offset);
					long npos = output.Seek(offset,SeekOrigin.Begin);
					_Debug("new pos="+npos);
				}
			}
	
			sendCommand("RETR " + remFileName);
			if(!(retValue == 150 || retValue == 125))
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
	
			while(true) {
				bytes = cSocket.Receive(buffer, buffer.Length, 0);
				output.Write(buffer,0,bytes);
				if(bytes <= 0)
					break;
			}
	
			output.Close();
			if (cSocket.Connected)
				cSocket.Close();
			_Debug("");
			readReply();
			if( !(retValue == 226 || retValue == 250) )
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
		}

		/// <summary>Upload a file.</summary>
		/// <param name="fileName">The name of the file</param>
		public void upload(string fileName) {
			upload(fileName,false);
		}
		/// <summary>Upload a file and set the resume flag.</summary>
		/// <param name="fileName">The name of the file</param>
		/// <param name="resume">Resume flag</param>
		public void upload(string fileName,Boolean resume) {
			if(!logined)
				login();
	
			Socket cSocket = createDataSocket();
			long offset=0;
	
			if(resume) {
				try {
					setBinaryMode(true);
					offset = getFileSize(fileName);
				} catch(Exception) {
					offset = 0;
				}
			}
	
			if(offset > 0 ) {
				sendCommand("REST " + offset);
				if(retValue != 350) {
					//throw(new x_exception("xFtp::" + retValue.ToString(), reply));
					//Remote server may not support resuming.
					offset = 0;
				}
			}
	
			sendCommand("STOR "+Path.GetFileName(fileName));
			if( !(retValue == 125 || retValue == 150) )
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
	
			// open input stream to read source file
			FileStream input = new FileStream(fileName,FileMode.Open);
			if(offset != 0) {
				if(debug)
					_Debug("seeking to " + offset);
				input.Seek(offset,SeekOrigin.Begin);
			}
	
			_Debug("Uploading file "+fileName+" to "+remotePath);
			while ((bytes = input.Read(buffer,0,buffer.Length)) > 0) {
				cSocket.Send(buffer, bytes, 0);
			}
			input.Close();
			_Debug("");
			if (cSocket.Connected)
				cSocket.Close();
	
			readReply();
			if( !(retValue == 226 || retValue == 250) )
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
		}

		/// <summary>Delete a file from the remote FTP server.</summary>
		/// <param name="fileName">The name of the file</param>
		public void deleteRemoteFile(string fileName) {
			if(!logined)
				login();
			sendCommand("DELE "+fileName);
	
			if(retValue != 250)
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
		}

		/// <summary>Rename a file on the remote FTP server.</summary>
		/// <param name="oldFileName">The old name of the file</param>
		/// <param name="newFileName">The new name of the file</param>
		public void renameRemoteFile(string oldFileName,string newFileName) {
			if(!logined)
				login();
			sendCommand("RNFR "+oldFileName);
			if(retValue != 350)
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
			//  known problem
			//  rnto will not take care of existing file.
			//  i.e. It will overwrite if newFileName exist
			sendCommand("RNTO "+newFileName);
			if(retValue != 250)
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
		}

		/// <summary>Create a directory on the remote FTP server.</summary>
		/// <param name="dirName">The name of the directory to be created</param>
		public void mkdir(string dirName) {
			if(!logined)
				login();
			sendCommand("MKD "+dirName);
			if(retValue != 250)
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
		}

		/// <summary>Delete a directory on the remote FTP server.</summary>
		/// <param name="dirName">The name of the directory to be deleted</param>
		public void rmdir(string dirName) {
			if(!logined)
				login();
			sendCommand("RMD "+dirName);
			if(retValue != 250)
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
		}

		/// <summary>Change the current working directory on the remote FTP server.</summary>
		/// <param name="dirName">The name of the directory to be made current</param>
		public void chdir(string dirName) {
			if(dirName.Equals("."))
				return;
			if(!logined)
				login();
			sendCommand("CWD "+dirName);
			if(retValue != 250)
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
	
			this.remotePath = dirName;
			_Debug("Current directory is "+remotePath);
		}

		/// <summary>Close the FTP connection.</summary>
		public void close() {
			if( clientSocket != null )
				sendCommand("QUIT");
			cleanup();
			_Debug("Closing...");
		}

		/// <summary>Set debug mode.</summary>
		/// <param name="debug">Debugging flag</param>
		public void setDebug(Boolean debug) {
			this.debug = debug;
		}
		#endregion
		
		#region Protected methods
		#endregion
				
		#region Private methods
		private void readReply() {
			mes = "";
			reply = readLine();
			retValue = Int32.Parse(reply.Substring(0,3));
		}

		private void cleanup() {
			if(clientSocket!=null) {
				clientSocket.Close();
				clientSocket = null;
			}
			logined = false;
		}

		private string readLine() {
			while(true) {
				bytes = clientSocket.Receive(buffer, buffer.Length, 0);
				mes += ASCII.GetString(buffer, 0, bytes);
				if(bytes < buffer.Length)
					break;
			}
	
			char[] seperator = {'\n'};
			string[] mess = mes.Split(seperator);
	
			if(mes.Length > 2)
				mes = mess[mess.Length-2];
			else
				mes = mess[0];
	
			if(!mes.Substring(3,1).Equals(" "))
				return readLine();
	
			if(debug) {
				for(int k=0;k < mess.Length-1;k++)
					_Debug(mess[k]);
			}
			return mes;
		}

		private void sendCommand(String command) {
			Byte[] cmdBytes = Encoding.ASCII.GetBytes((command+"\r\n").ToCharArray());
			clientSocket.Send(cmdBytes, cmdBytes.Length, 0);
			readReply();
		}

		private Socket createDataSocket() {
			sendCommand("PASV");
			if(retValue != 227)
				throw(new x_exception("xFtp::" + retValue.ToString(), reply));
	
			int index1 = reply.IndexOf('(');
			int index2 = reply.IndexOf(')');
			string ipData = reply.Substring(index1+1,index2-index1-1);
			int[] parts = new int[6];
	
			int len = ipData.Length;
			int partCount = 0;
			string buf="";
	
			for (int i = 0; i < len && partCount <= 6; i++) {
				char ch = Char.Parse(ipData.Substring(i,1));
				if (Char.IsDigit(ch))
					buf+=ch;
				else if (ch != ',')
					throw(new x_exception("xFtp::" + "Malformed PASV reply", reply));
	
				if (ch == ',' || i+1 == len) {
					try {
						parts[partCount++] = Int32.Parse(buf);
						buf="";
					} catch (Exception) {
						throw(new x_exception("xFtp::" + "Malformed PASV reply", reply));
					}
				}
			}
			string ipAddress = parts[0] + "."+ parts[1]+ "." + parts[2] + "." + parts[3];
			int port = (parts[4] << 8) + parts[5];
			Socket s = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
			IPEndPoint ep = new IPEndPoint(Dns.GetHostEntry(ipAddress).AddressList[0], port);
			try {
				s.Connect(ep);
			} catch(Exception) {
				throw(new x_exception("xFtp::", "Can't connect to remote server"));
			}
			return s;
		}
		private void _Debug(string msg) {
			_message += " - " + msg;
		}
		#endregion
	}
}

